﻿package com.yahoo.astra.fl.charts
{
	import com.yahoo.astra.fl.charts.series.ISeries;
	import com.yahoo.astra.utils.DateUtil;
	import com.yahoo.astra.utils.TimeUnit;
	
	import fl.core.InvalidationType;
	import fl.core.UIComponent;
	
	import flash.display.Sprite;
	import flash.geom.Point;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	
	/**
	 * An axis type representing a date and time range. Labels appear on major units.
	 * Ticks and grid lines may optionally appear on major and minor units.
	 * 
	 * @author Josh Tynjala
	 */
	public class TimeAxis extends Axis
	{
		
	//--------------------------------------
	//  Constructor
	//--------------------------------------
	
		/**
		 * Constructor.
		 */
		public function TimeAxis()
		{
			super(this);
		}
		
	//--------------------------------------
	//  Variables and Properties
	//--------------------------------------
		
		/**
		 * @private
		 * The drawing canvas for the major grid lines.
		 */
		public var gridLines:Sprite;
		
		/**
		 * @private
		 * The drawing canvas for the minor grid lines.
		 */
		public var minorGridLines:Sprite;
		
		/**
		 * @private
		 * A label used to estimate size.
		 */
		private var sampleLabel:TextField = new TextField();
		
	//-- Scale
		
		/**
		 * @private
		 * Storage for the minimum value.
		 */
		private var _minimum:Date;
		
		/**
		 * @private
		 * Indicates whether the minimum bound is user-defined or generated by the axis.
		 */
		private var _minimumSetByUser:Boolean = false;
		
		/**
		 * The minimum value displayed on the axis.
		 */
		public function get minimum():Date
		{
			return this._minimum;
		}
		
		/**
		 * @private
		 */
		public function set minimum(value:Date):void
		{
			if(this._minimum != value || !this._minimumSetByUser)
			{
				this._minimum = value;
				this._minimumSetByUser = value != null;
				this.invalidate();
			}
		}
	
		/**
		 * @private
		 * Storage for the maximum value.
		 */
		private var _maximum:Date;
		
		/**
		 * @private
		 * Indicates whether the maximum bound is user-defined or generated by the axis.
		 */
		private var _maximumSetByUser:Boolean = false;
		
		/**
		 * The maximum value displayed on the axis.
		 */
		public function get maximum():Date
		{
			return this._maximum;
		}
		
		/**
		 * @private
		 */
		public function set maximum(value:Date):void
		{
			if(this._maximum != value || !this._maximumSetByUser)
			{
				this._maximum = value;
				this._maximumSetByUser = value != null;
				this.invalidate();
			}
		}
	
	//-- Units
	
		/**
		 * @private
		 * Storage for the major unit.
		 */
		private var _majorUnit:int = 1;
		
		/**
		 * @private
		 * Indicates whether the major unit is user-defined or generated by the axis.
		 */
		private var _majorUnitSetByUser:Boolean = false;
		
		/**
		 * The major unit at which new lines are drawn.
		 */
		public function get majorUnit():Number
		{
			return this._majorUnit;
		}
		
		/**
		 * @private
		 */
		public function set majorUnit(value:Number):void
		{
			if(this._majorUnit != value || !this._majorUnitSetByUser)
			{
				this._majorUnit = value;
				this._majorUnitSetByUser = !isNaN(value);
				this.invalidate();
			}
		}
	
		/**
		 * @private
		 * Storage for the majorTimeUnit property.
		 */
		private var _majorTimeUnit:String = TimeUnit.MONTH;
		
		/**
		 * @private
		 * Indicates whether the major time unit is user-defined or generated by the axis.
		 */
		private var _majorTimeUnitSetByUser:Boolean = false;
		
		/**
		 * Combined with majorUnit, determines the amount of time between major ticks and labels.
		 * 
		 * @see com.yahoo.astra.fl.charts.TimeUnit;
		 */
		public function get majorTimeUnit():String
		{
			return this._majorTimeUnit;
		}
		
		/**
		 * @private
		 */
		public function set majorTimeUnit(value:String):void
		{
			if(this._majorTimeUnit != value || !this._majorTimeUnitSetByUser)
			{
				this._majorTimeUnit = value;
				this._majorTimeUnitSetByUser = value != null;
				this.invalidate();
			}
		}
	
		/**
		 * @private
		 * Storage for the minor unit.
		 */
		private var _minorUnit:int = 1;
		
		/**
		 * @private
		 * Indicates whether the minor unit is user-defined or generated by the axis.
		 */
		private var _minorUnitSetByUser:Boolean = false;
		
		/**
		 * The minor unit at which new lines are drawn.
		 */
		public function get minorUnit():Number
		{
			return this._minorUnit;
		}
		
		/**
		 * @private
		 */
		public function set minorUnit(value:Number):void
		{
			if(this._minorUnit != value || !this._minorUnitSetByUser)
			{
				this._minorUnit = value;
				this._minorUnitSetByUser = !isNaN(value);
				this.invalidate();
			}
		}
	
		/**
		 * @private
		 * Storage for the minorTimeUnit property.
		 */
		private var _minorTimeUnit:String = TimeUnit.MONTH;
		
		/**
		 * @private
		 * Indicates whether the minor time unit is user-defined or generated by the axis.
		 */
		private var _minorTimeUnitSetByUser:Boolean = false;
		
		/**
		 * Combined with minorUnit, determines the amount of time between minor ticks.
		 * 
		 * @see com.yahoo.astra.fl.charts.TimeUnit;
		 */
		public function get minorTimeUnit():String
		{
			return this._minorTimeUnit;
		}
		
		/**
		 * @private
		 */
		public function set minorTimeUnit(value:String):void
		{
			if(this._minorTimeUnit != value || !this._minorTimeUnitSetByUser)
			{
				this._minorTimeUnit = value;
				this._minorTimeUnitSetByUser = value != null;
				this.invalidate();
			}
		}
	
		/**
		 * @private
		 * Storage for the snapToUnits property.
		 */
		private var _snapToUnits:Boolean = true;
		
		/**
		 * If true, the labels, ticks, gridlines, and other objects will snap to
		 * the nearest major or minor unit. If false, their position will be based
		 * on the minimum value.
		 */
		public function get snapToUnits():Boolean
		{
			return this._snapToUnits;
		}
		
		/**
		 * @private
		 */
		public function set snapToUnits(value:Boolean):void
		{
			if(this._snapToUnits != value)
			{
				this._snapToUnits = value;
				this.invalidate();
			}
		}
		
	//-- Labels
	
		/**
		 * @private
		 * Storage for the labels.
		 */
		protected var labels:Array = [];
	
		/**
		 * @private
		 * The maximum size of the labels. Comes from the width property when
		 * the axis is vertical, or the height if it is horizontal.
		 */
		private var _maxLabelSize:Number = 0;
		
	//-- Misc
	
		/**
		 * @private
		 * A multiplier used to convert from a data value to a position on the axis.
		 */
		private var _positionMultiplier:Number = 1;
		
		/**
		 * @private
		 */
		private var _dataMinimum:Date;
		
		/**
		 * @private
		 */
		private var _dataMaximum:Date;
		
	//--------------------------------------
	//  Public Methods
	//--------------------------------------
	
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#valueToLocal()
		 */
		override public function valueToLocal(data:Object):Number
		{
			var value:Number = 0;
			if(data is Date)
			{
				value = (data as Date).valueOf();
			}
			else(!(data is Number))
			{
				value = new Date(data.toString()).valueOf();
			}
			
			var position:Number = (value - this.minimum.valueOf()) * this._positionMultiplier;
			
			if(this.reverse && this.orientation == AxisOrientation.HORIZONTAL)
			{
				position = this._contentBounds.width - position;
			}
			else if(!this.reverse && this.orientation == AxisOrientation.VERTICAL)
			{
				position = this._contentBounds.height - position;
			}
				
			return position;
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#localToValue()
		 */
		override public function localToValue(position:Number):Object
		{
			if(this.reverse && this.orientation == AxisOrientation.HORIZONTAL)
			{
				position = this._contentBounds.width + position;
			}
			else if(!this.reverse && this.orientation == AxisOrientation.VERTICAL)
			{
				position = this._contentBounds.height + position;
			}
			return (position / this._positionMultiplier) + this.minimum.valueOf();
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#valueToLabel();
		 */
		override public function valueToLabel(value:Object):String
		{
			var text:String = value.toString();
			if(this.labelFunction != null)
			{
				if(value is Number)
				{
					value = new Date(value);
				}
				else if(!(value is Date))
				{
					value = new Date(text);
				}
				text = this.labelFunction(value, this.majorTimeUnit);
			}			
			if(text == null) text = "";
			return text;
		}
		
		/**
		 * @private
		 */
		override public function invalidate(property:String = InvalidationType.ALL, callLater:Boolean = true):void
		{
			//we need to make sure direct changes to this axis cause the whole chart to redraw!
			if(!inCallLaterPhase && this.parent is UIComponent) 
			{
				UIComponent(this.parent).invalidate();
			}
			super.invalidate(property, callLater);
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#updateScale()
		 */
		override public function updateScale(data:Array):void
		{
			var seriesCount:int = data.length;
			var min:Number = NaN;
			var max:Number = NaN;
			for(var i:int = 0; i < seriesCount; i++)
			{
				var series:ISeries = ISeries(data[i]);
				var seriesLength:int = series.length;
				var dataField:String = this.plotArea.axisAndSeriesToField(this, series);
				for(var j:int = 0; j < seriesLength; j++)
				{
					var item:Object = series.dataProvider[j];
					var value:Number = NaN;
					if(dataField && item.hasOwnProperty(dataField))
					{
						item = item[dataField];
					}
					
					if(item is Date)
					{
						value = (item as Date).valueOf();
					}
					else if(item is Number)
					{
						value = item as Number;
					}
					else
					{
						value = new Date(item.toString()).valueOf();
					}
					//skip bad data
					if(isNaN(value)) continue;
					
					if(isNaN(min)) min = value;
					else min = Math.min(min, value);
					if(isNaN(max)) max = value;
					else max = Math.max(max, value);
				}
			}
			
			//bad data. show yesterday through today.
			if(isNaN(min) || isNaN(max))
			{
				var today:Date = new Date();
				max = today.valueOf();
				today.setDate(today.getDate() - 1);
				min = today.valueOf();
			}
			
			this._dataMinimum = new Date(min);
			this._dataMaximum = new Date(max);
			
			this.resetScale();
		}
		
		
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#updateScale()
		 */
		override public function updateBounds():void
		{
			this.refreshLabels();
			
			super.updateBounds();
			
			this.calculatePositionMultiplier();
		}
		
	//--------------------------------------
	//  Protected Methods
	//--------------------------------------
		
		/**
		 * @private
		 */
		override protected function draw():void
		{
			super.draw();
			
			this.updateBounds();
			
			this.graphics.clear();
			this.drawAxis();
			this.drawObjectsOnMinorUnit();
			this.drawObjectsOnMajorUnit();
		}
		
		/**
		 * If the minimum, maximum, major unit or minor unit have not been set by the user,
		 * these values must be generated by the axis. May be overridden to use custom
		 * scaling algorithms.
		 */
		protected function resetScale():void
		{
			if(!this._minimumSetByUser)
			{
				this._minimum = new Date(this._dataMinimum.valueOf());
			}
				
			if(!this._maximumSetByUser)
			{
				this._maximum = new Date(this._dataMaximum.valueOf());
			}
			
			//swap min and max if needed
			if(this._minimum.valueOf() > this._maximum.valueOf())
			{
				var temp:Date = this._minimum;
				this._minimum = this._maximum;
				this._maximum = temp;
				
				//be sure to swap these flags too!
				var temp2:Boolean = this._minimumSetByUser;
				this._minimumSetByUser = this._maximumSetByUser;
				this._maximumSetByUser = temp2;
			}
				
			var lengthOfAxis:Number = this.width;
			if(this.orientation == AxisOrientation.VERTICAL)
			{
				lengthOfAxis = this.height;
			}
			
			var timeUnits:Array = [TimeUnit.MILLISECONDS, TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAY, TimeUnit.MONTH, TimeUnit.YEAR];
			if(!this._majorTimeUnitSetByUser)
			{
				//ballpark it
				var dayCount:Number = DateUtil.countDays(this.minimum, this.maximum);
				var yearCount:Number = dayCount / 365;
				var monthCount:Number = yearCount * 12;
				var hourCount:Number = dayCount * 24;
				var minuteCount:Number = hourCount * 60;
				var secondCount:Number = minuteCount * 60;
				
				if(yearCount >= 1) this._majorTimeUnit = TimeUnit.YEAR;
				else if(monthCount >= 1) this._majorTimeUnit = TimeUnit.MONTH;
				else if(dayCount >= 1) this._majorTimeUnit = TimeUnit.DAY;
				else if(hourCount >= 1) this._majorTimeUnit = TimeUnit.HOURS;
				else if(minuteCount >= 1) this.majorTimeUnit = TimeUnit.MINUTES;
				else if(secondCount >= 1) this.majorTimeUnit = TimeUnit.SECONDS; 
				else this.majorTimeUnit = TimeUnit.MILLISECONDS;
			}	
			var unitIndex:int = timeUnits.indexOf(this._majorTimeUnit);
			var timeUnitSize:Number = this.calculateTimeUnitSize(this.majorTimeUnit);
			
			if(!this._majorUnitSetByUser)
			{
				this._majorUnit = 0;
				var savedMinimum:Date = new Date(this._minimum.valueOf());
				var savedMaximum:Date = new Date(this._maximum.valueOf());
				
				do
				{
					//only change the major time unit if the user hasn't set it
					//it may give us horrible intervals, but that's what the user wants
					if(!this._majorTimeUnitSetByUser && this._majorUnit == 10 && unitIndex < timeUnits.length - 1)
					{
						unitIndex++;
						this._majorTimeUnit = timeUnits[unitIndex];
						timeUnitSize = this.calculateTimeUnitSize(this.majorTimeUnit);
						this._majorUnit = 0;
					}
					this._majorUnit++;
					
					//if we're looping around again, restore the old minimum and maximum
					//so that the bounds don't grow out of control when generated from the
					//major unit.
					this._minimum = new Date(savedMinimum.valueOf());
					this._maximum = new Date(savedMaximum.valueOf());
					
					//based on the new major unit, calculate the minimum and maximum
					this.calculateMaximumAndMinimum();
										
					//while the major unit is dependent on the minimum and maximum,
					//it is also dependent on the label size because we don't want
					//overlapping labels!
					var labelBounds:Point = this.calculateMaximumLabelBounds(this._majorUnit);
					if(this.orientation == AxisOrientation.VERTICAL)
					{
						var minimumSpacing:Number = Math.max(50, labelBounds.y);
					}
					else minimumSpacing = Math.max(50, labelBounds.x);
					var majorUnitSpacing:Number = lengthOfAxis / (1 + (this.maximum.valueOf() - this.minimum.valueOf()) / (this._majorUnit * timeUnitSize));
					
				}
				while(majorUnitSpacing < minimumSpacing && majorUnitSpacing < lengthOfAxis)
				//using a while loop shouldn't cause any problems unless the axis
				//bounds are seperated by an inordinate number of years. it starts at one year
				//intervals and needs to check the bounds for every label that appears.
				//TODO: See if we can account for this case.
			}
			
			if(!this._minorTimeUnitSetByUser)
			{
				//if the numeric part of the major unit is 1, we want to move
				//the time part of the minor unit to a interval lower than the major.
				//...unless the user has set the minor unit. this is a weird case
				//that shouldn't happen, but it might.
				//in that case, we go with the standard behavior where major unit
				//and minor unit are the same.
				if(!this._minorUnitSetByUser && this._majorUnit == 1)
				{
					var index:int = timeUnits.indexOf(this._majorTimeUnit);
					if(index > 0) this._minorTimeUnit = timeUnits[index - 1];
				}
				else this._minorTimeUnit = this._majorTimeUnit;
			}
			
			if(!this._minorUnitSetByUser)
			{
				if(this.majorTimeUnit == this.minorTimeUnit && this._majorUnit != 1)
				{
					if(this._majorUnit % 2 == 0)
					{
						this._minorUnit = this._majorUnit / 2;
					}
					else if(this._majorUnit % 3 == 0)
					{
						this._minorUnit = this._majorUnit / 3;
					}
					else this._minorUnit = 0;
				}
				else
				{
					//in this case, we know that the time portion of the minor
					//unit is a smaller interval than the major unit.
					switch(this._minorTimeUnit)
					{
						case TimeUnit.MONTH:
							this._minorUnit = 6;
							break;
							
						/*case TimeUnit.DAY:
							//no perfect half-way point for number of days in a month
							this._minorUnit = 0;
							break;*/
							
						case TimeUnit.HOURS:
							this._minorUnit = 12;
							break;
							
						case TimeUnit.MINUTES:
							this._minorUnit = 30;
							break;
							
						case TimeUnit.SECONDS:
							this._minorUnit = 30;
							break;
							
						default:
							this._minorUnit = 0;
							break;
					}
				}
			}
			
		}
		
		private function calculateTimeUnitSize(timeUnit:String):Number
		{
			switch(timeUnit)
			{
				case TimeUnit.YEAR:
					var year:Date = new Date(1970, 11, 31, 16);
					return year.valueOf();
				
				case TimeUnit.MONTH:
					var month:Date = new Date(1970, 0, 31, 16);
					return month.valueOf();
					
				case TimeUnit.DAY:
					var day:Date = new Date(1970, 0, 1, 16);
					return day.valueOf();
				
				case TimeUnit.HOURS:
					var hour:Date = new Date(1969, 11, 31, 17);
					return hour.valueOf();
					
				case TimeUnit.MINUTES:
					var minute:Date = new Date(1969, 11, 31, 16, 1);
					return minute.valueOf();
					
				case TimeUnit.SECONDS:
					var second:Date = new Date(1969, 11, 31, 16, 0, 1);
					return second.valueOf();
					
				default: //millisecond
					return 1;
				
			}
		}
		
		/**
		 * @private
		 * Using the major time unit, and the current minimum and maximum, generate
		 * the ideal minimum and maximum.
		 */
		private function calculateMaximumAndMinimum():void
		{
			switch(this.majorTimeUnit)
			{
				case TimeUnit.YEAR:
				{
					if(!this._minimumSetByUser)
					{
						this._minimum = new Date(this._minimum.fullYear, 0);
					}
						
					if(!this._maximumSetByUser)
					{
						var beginningOfYear:Date = new Date(this._maximum.fullYear, 0);
						//don't change the maximum if it is the exact beginning of the year
						if(beginningOfYear.valueOf() != this._maximum.valueOf())
							this._maximum = new Date(this._maximum.fullYear + 1, 0);
					}						
					break;
				}
				case TimeUnit.MONTH:
				{
					if(!this._minimumSetByUser)
						this._minimum = new Date(this._minimum.fullYear, this._minimum.month);
						
					if(!this._maximumSetByUser)
					{
						var beginningOfMonth:Date = new Date(this._maximum.fullYear, this._maximum.month);
						//don't change the maximum if it is the exact beginning of the month
						if(beginningOfMonth.valueOf() != this._maximum.valueOf())
							this._maximum = new Date(this._maximum.fullYear, this._maximum.month + 1);
					}
					break;
				}
				case TimeUnit.DAY:
				{
					if(!this._minimumSetByUser)
						this._minimum = new Date(this._minimum.fullYear, this._minimum.month, this._minimum.date);
						
					if(!this._maximumSetByUser)
					{
						var beginningOfDay:Date = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date);
						//don't change the maximum if it is the exact beginning of the day
						if(beginningOfDay.valueOf() != this._maximum.valueOf())
							this._maximum = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date + 1);
					}
					break;
				}
				case TimeUnit.HOURS:
				{
					if(!this._minimumSetByUser)
						this._minimum = new Date(this._minimum.fullYear, this._minimum.month, this._minimum.date, this._minimum.hours);
						
					if(!this._maximumSetByUser)
					{
						var beginningOfHour:Date = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date, this._maximum.hours);
						//don't change the maximum if it is the exact beginning of the day
						if(beginningOfHour.valueOf() != this._maximum.valueOf())
							this._maximum = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date, this._maximum.hours + 1);
					}
					break;
				}
				case TimeUnit.MINUTES:
				{
					if(!this._minimumSetByUser)
						this._minimum = new Date(this._minimum.fullYear, this._minimum.month, this._minimum.date, this._minimum.hours, this._minimum.minutes);
						
					if(!this._maximumSetByUser)
					{
						var beginningOfMinute:Date = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date, this._maximum.hours, this._maximum.minutes);
						//don't change the maximum if it is the exact beginning of the day
						if(beginningOfMinute.valueOf() != this._maximum.valueOf())
							this._maximum = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date, this._maximum.hours, this._maximum.minutes + 1);
					}
					break;
				}
				case TimeUnit.SECONDS:
				{
					if(!this._minimumSetByUser)
						this._minimum = new Date(this._minimum.fullYear, this._minimum.month, this._minimum.date, this._minimum.hours, this._minimum.minutes, this._minimum.seconds);
						
					if(!this._maximumSetByUser)
					{
						var beginningOfSecond:Date = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date, this._maximum.hours, this._maximum.minutes, this._maximum.seconds);
						//don't change the maximum if it is the exact beginning of the day
						if(beginningOfSecond.valueOf() != this._maximum.valueOf())
							this._maximum = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date, this._maximum.hours, this._maximum.minutes, this._maximum.seconds + 1);
					}
					break;
				}
			}
		}
		
		protected function calculateMaximumLabelBounds(proposedMajorUnit:Number):Point
		{
			this.sampleLabel.defaultTextFormat = this.getStyleValue("textFormat") as TextFormat;
			this.sampleLabel.autoSize = TextFieldAutoSize.LEFT;
			
			var maximumBounds:Point = new Point();
			
			var value:Date = new Date(this.minimum.valueOf());
			var maximumValue:Number = this.maximum.valueOf();
			while(value.valueOf() <= maximumValue)
			{	
				switch(this.majorTimeUnit)
				{
					case TimeUnit.YEAR:
						value.fullYear += proposedMajorUnit;
						break;
					case TimeUnit.MONTH:
						value.month += proposedMajorUnit;
						break;
					case TimeUnit.DAY:
						value.date += proposedMajorUnit;
						break;
					case TimeUnit.HOURS:
						value.hours += proposedMajorUnit;
						break;
					case TimeUnit.MINUTES:
						value.minutes += proposedMajorUnit;
						break;
					case TimeUnit.SECONDS:
						value.seconds += proposedMajorUnit;
						break;
					case TimeUnit.MILLISECONDS:
						value.milliseconds += proposedMajorUnit;
						break;
				}
				
				var text:String = this.valueToLabel(value);
				var bounds:Point = this.measureLabel(text);
				maximumBounds.x = Math.max(maximumBounds.x, bounds.x);
				maximumBounds.y = Math.max(maximumBounds.y, bounds.y);
			}
			
			return maximumBounds;
		}
		
		/**
		 * @private
		 * Calculates the bounds of a label with the specified text.
		 */
		protected function measureLabel(text:String):Point
		{
			this.sampleLabel.text = text;
			return new Point(this.sampleLabel.width, this.sampleLabel.height);
		}
	
		/**
		 * @copy com.yahoo.astra.fl.charts.Axis#calculateContentBounds()
		 */
		override protected function calculateContentBounds():void
		{
			super.calculateContentBounds();
			
			if(this.labels.length > 0)
			{
				var firstLabel:TextField = this.labels[0];
				var lastLabel:TextField = this.labels[this.labels.length - 1];
			
				if(this.reverse)
				{
					var temp:TextField = firstLabel;
					firstLabel = lastLabel;
					lastLabel = temp;
				}
			}
			
			var labelDistance:Number = this.getStyleValue("labelDistance") as Number;
			var showTicks:Boolean = this.getStyleValue("showTicks");
			var showMinorTicks:Boolean = this.getStyleValue("showMinorTicks");
			var tickLength:Number = this.getStyleValue("tickLength") as Number;
			var minorTickLength:Number = this.getStyleValue("minorTickLength") as Number;
			var tickPosition:String = this.getStyleValue("tickPosition") as String;
			var minorTickPosition:String = this.getStyleValue("minorTickPosition") as String;
			if(this.orientation == AxisOrientation.VERTICAL)
			{
				var firstLabelHeight:Number = 0;
				var lastLabelHeight:Number = 0;
				if(this.labels.length > 0)
				{
					if(firstLabel.text.length > 0) firstLabelHeight = firstLabel.height;
					if(lastLabel.text.length > 0) lastLabelHeight = lastLabel.height;
				}
				
				var contentBoundsX:Number = this._maxLabelSize + labelDistance;
				
				var tickContentBoundsX:Number = 0;
				if(showTicks)
				{
					switch(tickPosition)
					{
						case TickPosition.OUTSIDE:
						case TickPosition.CROSS:
							tickContentBoundsX = tickLength;
							break;
					}
				}
				if(showMinorTicks)
				{
					switch(minorTickPosition)
					{
						case TickPosition.OUTSIDE:
						case TickPosition.CROSS:
							tickContentBoundsX = Math.max(tickContentBoundsX, minorTickLength);
					}
				}
				contentBoundsX += tickContentBoundsX;
				this._contentBounds.x += contentBoundsX;
				
				var contentBoundsY:Number = firstLabelHeight / 2;
				this._contentBounds.y += contentBoundsY;
				this._contentBounds.width -= contentBoundsX;
				this._contentBounds.height -= ((lastLabelHeight / 2) + contentBoundsY);
			}
			else
			{
				var firstLabelWidth:Number = 0;
				var lastLabelWidth:Number = 0;
				if(this.labels.length > 0)
				{
					if(firstLabel.text.length > 0) firstLabelWidth = firstLabel.width;
					if(lastLabel.text.length > 0) lastLabelWidth = lastLabel.width;
				}
				
				//TODO: Watch out for this one... (ceil)
				contentBoundsX = Math.ceil(firstLabelWidth / 2);
				this._contentBounds.x += contentBoundsX;
				this._contentBounds.width -= ((lastLabelWidth / 2) + contentBoundsX);
				this._contentBounds.height -= (this._maxLabelSize + labelDistance);
				
				var tickHeight:Number = 0;
				if(showTicks)
				{
					switch(tickPosition)
					{
						case TickPosition.OUTSIDE:
						case TickPosition.CROSS:
							tickHeight = tickLength;
							break;
					}
				}
				if(showMinorTicks)
				{
					switch(minorTickPosition)
					{
						case TickPosition.OUTSIDE:
						case TickPosition.CROSS:
							tickHeight = Math.max(tickHeight, minorTickLength);
					}
				}
				
				this._contentBounds.height -= tickHeight;
			}
		}
	
		/**
		 * Redraws the main axis.
		 */
		protected function drawAxis():void
		{
			var min:Number = this.valueToLocal(this.minimum);
			var max:Number = this.valueToLocal(this.maximum);
			
			var axisWeight:int = this.getStyleValue("axisWeight") as int;
			var axisColor:uint = this.getStyleValue("axisColor") as uint;
			this.graphics.lineStyle(axisWeight, axisColor);
			if(this.orientation == AxisOrientation.VERTICAL)
			{
				this.graphics.moveTo(this._contentBounds.x, this._contentBounds.y + min);
				this.graphics.lineTo(this._contentBounds.x, this._contentBounds.y + max);
			}
			else //horizontal
			{
				this.graphics.moveTo(this._contentBounds.x + min, this._contentBounds.y + this._contentBounds.height);
				this.graphics.lineTo(this._contentBounds.x + max, this._contentBounds.y + this._contentBounds.height);
			}
		}
		
		/**
		 * Draws labels and ticks at the major unit positions.
		 */
		protected function drawObjectsOnMajorUnit():void
		{
			if(this.gridLines) this.gridLines.graphics.clear();
			if(this.majorUnit <= 0) return;
			
			var showLabels:Boolean = this.getStyleValue("showLabels") as Boolean;
			var labelDistance:Number = this.getStyleValue("labelDistance") as Number;
			var showGridLines:Boolean = this.getStyleValue("showGridLines") as Boolean;
			var gridLineWeight:int = this.getStyleValue("gridLineWeight") as int;
			var gridLineColor:uint = this.getStyleValue("gridLineColor") as uint;
			var showTicks:Boolean = this.getStyleValue("showTicks") as Boolean;
			var tickPosition:Boolean = this.getStyleValue("tickPosition") as String;
			var tickWeight:int = this.getStyleValue("tickWeight") as int;
			var tickColor:uint = this.getStyleValue("tickColor") as uint;
			var tickLength:Number = this.getStyleValue("tickLength") as Number;
			
			var lastVisibleLabel:TextField;
			var labelCount:int = this.labels.length;
			for(var i:int = 0; i < labelCount; i++)
			{
				var label:TextField = this.labels[i];
				var date:Date = new Date(this.minimum.valueOf());
				if(i > 0)
				{
					var unitValue:Number = i * this.majorUnit;
					date = this.updateDate(date, this._majorTimeUnit, unitValue, this.snapToUnits);
				}
				
				//stop at the maximum value.
				if(date.valueOf() > this.maximum.valueOf())
				{
					date = new Date(this.maximum.valueOf());
				}
				
				//because Flash UIComponents round the position to the nearest pixel, we need to do the same.
				var position:Number = Math.round(this.valueToLocal(date));
				
				//draw the grid line
				if(this.gridLines && showGridLines)
				{
					this.gridLines.graphics.lineStyle(gridLineWeight, gridLineColor);
					//the gridlines are positioned at (contentBounds.x, contentBounds.y)
					if(this.orientation == AxisOrientation.VERTICAL)
					{
						this.gridLines.graphics.moveTo(0, position);
						this.gridLines.graphics.lineTo(this.contentBounds.width, position);
					}
					else
					{
						this.gridLines.graphics.moveTo(position, 0);
						this.gridLines.graphics.lineTo(position, this._contentBounds.height);
					}
				}
				
				//position the label
				if(this.orientation == AxisOrientation.VERTICAL)
				{
					position += this._contentBounds.y;
					if(showLabels)
					{
						label.x = this._contentBounds.x - label.width - labelDistance;
						label.y = position - label.height / 2;
					}
				}
				else
				{
					position += this._contentBounds.x;
					if(showLabels)
					{
						label.x = position - label.width / 2;
						label.y = this._contentBounds.y + this._contentBounds.height + labelDistance;
					}
				}
				
				//draw the ticks
				if(showTicks)
				{
					this.graphics.lineStyle(tickWeight, tickColor);
					switch(tickPosition)
					{
						case TickPosition.OUTSIDE:
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x - tickLength, position);
								this.graphics.lineTo(this._contentBounds.x, position);
								if(showLabels) label.x -= tickLength;
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height + tickLength);
								if(showLabels) label.y += tickLength;
							}
							break;
						case TickPosition.INSIDE:
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x, position);
								this.graphics.lineTo(this._contentBounds.x + tickLength, position);
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height - tickLength);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height);
							}
							break;
						default: //CROSS
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x - tickLength, position);
								this.graphics.lineTo(this._contentBounds.x + tickLength, position);
								if(showLabels) label.x -= tickLength;
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height - tickLength);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height + tickLength);
								if(showLabels) label.y += tickLength;
							}
					}
				}
				
				if(showLabels && this.hideOverlappingLabels)
				{
					label.visible = true;
					if(this.orientation == AxisOrientation.VERTICAL)
					{
						if(lastVisibleLabel)
						{
							if(!this.reverse && label.y + label.height > lastVisibleLabel.y ||
								this.reverse && lastVisibleLabel.y + lastVisibleLabel.height > label.y)
							{
								label.visible = false;	
							}
						}
					}
					else
					{
						if(lastVisibleLabel)
						{
							if(this.reverse && label.x + label.width > lastVisibleLabel.x ||
								!this.reverse && lastVisibleLabel.x + lastVisibleLabel.width > label.x)
							{
								label.visible = false;	
							}
						}
					}	
					if(label.visible) lastVisibleLabel = label;
				}
			} //end for loop
		}
		
		/**
		 * @private
		 * Draws ticks and gridlines at the minor unit positions.
		 */
		protected function drawObjectsOnMinorUnit():void
		{
			if(this.minorGridLines) this.minorGridLines.graphics.clear();
			if(this.minorUnit <= 0) return;
			
			//if we're showing major ticks, we should hide minor ticks on the major unit.
			var showTicks:Boolean = this.getStyleValue("showTicks");
			var showMinorGridLines:Boolean = this.getStyleValue("showMinorGridLines") as Boolean;
			var minorGridLineWeight:int = this.getStyleValue("minorGridLineWeight") as int;
			var minorGridLineColor:uint = this.getStyleValue("minorGridLineColor") as uint;
			var showMinorTicks:Boolean = this.getStyleValue("showMinorTicks") as Boolean;
			var minorTickPosition:Boolean = this.getStyleValue("minorTickPosition") as String;
			var minorTickWeight:int = this.getStyleValue("minorTickWeight") as int;
			var minorTickColor:uint = this.getStyleValue("minorTickColor") as uint;
			var minorTickLength:Number = this.getStyleValue("minorTickLength") as Number;
			
			var displayedMaximum:Boolean = false;
			//var skipMajorUnits:Boolean = showTicks && this.majorUnit > 0;
			var date:Date = new Date(this.minimum.valueOf());
			while(date.valueOf() <= this.maximum.valueOf())
			{	
				//because Flash UIComponents round the position to the nearest pixel, we need to do the same.
				var position:Number = Math.round(this.valueToLocal(date));
				
				//draw the grid line
				if(this.minorGridLines && showMinorGridLines)
				{
					this.minorGridLines.graphics.lineStyle(minorGridLineWeight, minorGridLineColor);
					if(this.orientation == AxisOrientation.VERTICAL)
					{
						this.minorGridLines.graphics.moveTo(this.contentBounds.x, position);
						this.minorGridLines.graphics.lineTo(this.contentBounds.x + this.contentBounds.width, position);
					}
					else
					{
						this.minorGridLines.graphics.moveTo(position, this.contentBounds.y);
						this.minorGridLines.graphics.lineTo(position, this.contentBounds.y + this._contentBounds.height);
					}
				}
				
				if(this.orientation == AxisOrientation.VERTICAL)
				{
					position += this._contentBounds.y;
				}
				else
				{
					position += this._contentBounds.x;
				}
				
				//draw the ticks
				if(showMinorTicks)
				{
					this.graphics.lineStyle(minorTickWeight, minorTickColor);
					switch(minorTickPosition)
					{
						case TickPosition.OUTSIDE:
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x - minorTickLength, position);
								this.graphics.lineTo(this._contentBounds.x, position);
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height + minorTickLength);
							}
							break;
						case TickPosition.INSIDE:
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x, position);
								this.graphics.lineTo(this._contentBounds.x + minorTickLength, position);
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height - minorTickLength);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height);
							}
							break;
						default:
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x - minorTickLength / 2, position);
								this.graphics.lineTo(this._contentBounds.x + minorTickLength / 2, position);
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height - minorTickLength / 2);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height + minorTickLength / 2);
							}
					}
				}
				
				if(displayedMaximum) break;
				
				date = this.updateDate(date, this.minorTimeUnit, this.minorUnit, this.snapToUnits);
				
				//stop at the maximum value.
				if(date.valueOf() >= this.maximum.valueOf())
				{
					date = new Date(this.maximum.valueOf());
					displayedMaximum = true;
				}
				
			} //end while loop
		}
		
	//--------------------------------------
	//  Private Methods
	//--------------------------------------
	
		/**
		 * @private
		 * Our calendar system sucks. I'd like some nifty math tricks to do this stuff.
		 */
		private function updateDate(date:Date, timeUnit:String, unitValue:Number, snapToUnits:Boolean):Date
		{
			switch(timeUnit)
			{
				case TimeUnit.YEAR:
					date.fullYear += unitValue;
					if(snapToUnits)
					{
						date.month = 0;
						date.date = 1;
						date.hours = 0;
						date.minutes = 0;
						date.seconds = 0;
						date.milliseconds = 0;
					}
					break;
				case TimeUnit.MONTH:
					date.month += unitValue;
					if(snapToUnits)
					{
						date.date = 1;
						date.hours = 0;
						date.minutes = 0;
						date.seconds = 0;
						date.milliseconds = 0;
					}
					break;
				case TimeUnit.DAY:
					date.date += unitValue;
					if(snapToUnits)
					{
						date.hours = 0;
						date.minutes = 0;
						date.seconds = 0;
						date.milliseconds = 0;
					}
					break;
				case TimeUnit.HOURS:
					date.hours += unitValue;
					if(snapToUnits)
					{
						date.minutes = 0;
						date.seconds = 0;
						date.milliseconds = 0;
					}
					break;
				case TimeUnit.MINUTES:
					date.minutes += unitValue;
					if(snapToUnits)
					{
						date.seconds = 0;
						date.milliseconds = 0;
					}
					break;
				case TimeUnit.SECONDS:
					date.seconds += unitValue;
					if(snapToUnits)
					{
						date.milliseconds = 0;
					}
					break;
				case TimeUnit.MILLISECONDS:
					date.milliseconds += unitValue;
					break;
			}
			return date;
		}
		
		/**
		 * @private
		 * Update the labels by adding or removing some, setting the text, etc.
		 */
		private function refreshLabels():void
		{
			var showLabels:Boolean = this.getStyleValue("showLabels") as Boolean;
			
			//calculate the number of labels
			var range:Number = this.maximum.valueOf() - this.minimum.valueOf();
			var labelCount:int = showLabels ? this.calculateLabelCount() : 0;
			var difference:int = labelCount - this.labels.length;
			if(difference > 0)
			{
				//add new labels
				for(var i:int = 0; i < difference; i++)
				{
					var label:TextField = new TextField();
					label.selectable = false;
					label.autoSize = TextFieldAutoSize.LEFT;
					this.labels.push(label);
					this.addChild(label);
				}
			}
			else if(difference < 0)
			{
				//remove existing labels
				difference = Math.abs(difference);
				for(i = 0; i < difference; i++)
				{
					label = this.labels.pop() as TextField;
					this.removeChild(label);
				}
			}
			
			var textFormat:TextFormat = this.getStyleValue("textFormat") as TextFormat;
			for(i = 0; i < labelCount; i++)
			{
				label = this.labels[i] as TextField;
				label.defaultTextFormat = textFormat;
			}
			
			//with the labels in place, update the displayed text
			if(showLabels)
			{
				this.setLabelText();
			}
		}
	
		/**
		 * @private
		 * Determines the number of labels that are needed on the axis.
		 */
		private function calculateLabelCount():int
		{
			var showLabels:Boolean = this.getStyleValue("showLabels");
			if(!showLabels) return 0;
			
			var yearDifference:int = this._maximum.fullYear - this._minimum.fullYear;
			var monthDifference:int = this._maximum.month - this._minimum.month;
			
			var labelCount:int = 0;
			switch(this.majorTimeUnit)
			{
				case TimeUnit.YEAR:
					//the number of years represented in the difference.
					labelCount = Math.floor(yearDifference / this.majorUnit) + 1;
					break;
				case TimeUnit.MONTH:					
					//the number of months represented in the difference.
					var monthCount:int = yearDifference * 12 + monthDifference;
					labelCount = Math.floor(monthCount / this.majorUnit) + 1;
					break;
				case TimeUnit.DAY:
					//the number of days represented in the difference.
					var dayCount:int = DateUtil.countDays(this._maximum, this._minimum);
					labelCount = Math.floor(dayCount / this.majorUnit) + 1;
					break;
				case TimeUnit.HOURS:
					//the number of hours represented in the difference.
					dayCount = DateUtil.countDays(this._maximum, this._minimum);
					var hoursCount:int = this._maximum.hours - this._minimum.hours
					labelCount = Math.floor((dayCount * 24 + hoursCount) / this.majorUnit) + 1;
					break;
				case TimeUnit.MINUTES:
					//the number of hours represented in the difference.
					dayCount = DateUtil.countDays(this._maximum, this._minimum);
					hoursCount = this._maximum.hours - this._minimum.hours;
					var minutesCount:int = this._maximum.minutes - this._minimum.minutes;
					labelCount = Math.floor(((dayCount * 24 + hoursCount) * 60 + minutesCount) / this.majorUnit) + 1;
					break;
				case TimeUnit.SECONDS:
					//the number of hours represented in the difference.
					dayCount = DateUtil.countDays(this._maximum, this._minimum);
					hoursCount = this._maximum.hours - this._minimum.hours;
					minutesCount = this._maximum.minutes - this._minimum.minutes;
					var secondsCount:int = this._maximum.seconds - this._minimum.seconds;
					labelCount = Math.floor((((dayCount * 24 + hoursCount) * 60 + minutesCount) * 60 + secondsCount) / this.majorUnit) + 1;
					break;
				default: //milliseconds
					labelCount = this.maximum.valueOf() - this.minimum.valueOf() + 1;
					break;
			}
			if(this.snapToUnits) labelCount++;
			return labelCount;
		}
		
		/**
		 * @private
		 * Sets the text for each label and determines the offets.
		 */
		private function setLabelText():void
		{
			this._maxLabelSize = 0;
			var labelCount:int = this.labels.length;	
			var maximumValue:Number = this.maximum.valueOf();
			for(var i:int = 0; i < labelCount; i++)
			{
				var label:TextField = this.labels[i] as TextField;
				var value:Date = new Date(this.minimum.valueOf());
				
				//skip the minimum
				if(i > 0)
				{
					var unitValue:Number = i * this.majorUnit;
					value = this.updateDate(value, this.majorTimeUnit, unitValue, this.snapToUnits);
				}
				
				if(value.valueOf() > maximumValue)
				{
					value = new Date(maximumValue);
				}
				
				label.text = this.valueToLabel(value);
				
				if(this.orientation == AxisOrientation.VERTICAL)
				{
					this._maxLabelSize = Math.max(this._maxLabelSize, label.width);
				}
				else this._maxLabelSize = Math.max(this._maxLabelSize, label.height);
			}
		}
		
		/**
		 * @private
		 * Calculates the multiplier used to convert a data point to an actual position
		 * on the axis.
		 */
		private function calculatePositionMultiplier():void
		{
			var range:Number = this._maximum.valueOf() - this._minimum.valueOf();
			if(range == 0)
			{
				this._positionMultiplier = 0;
				return;
			}
			
			var length:Number = this._contentBounds.width;
			if(this.orientation == AxisOrientation.VERTICAL)
			{
				length = this._contentBounds.height;
			}
			this._positionMultiplier = length / range;
		}
	
	
	}
}